#
# Copyright 2008-2010 Vadim Zhukov <persgray@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY AGAVA COMPANY ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL THE AGAVA COMPANY OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of the AGAVA Company.

package RB::Dir;

use bytes;
use strict;

use Errno;
use Exporter qw/import/;
use File::Spec::Functions qw/splitdir/;
use IO::Handle;
use POSIX qw/strftime/;
use RB::Archive;
use RB::Log;
use RB::Util;

our @EXPORT = qw/constructBackupLines printArchiveContents processArchiveInfo
                 scanBackupDir/;


# For consistency
my $Today = strftime("%Y%m%d", localtime);


# For each archive given this routine constructs a "backup line". Backup line is
# a list of archives needed to be extracted to get full backup for the date
# specified. Order of archives in a line is meaningful.
#
# Arguments:
#   @archives - list of RB::Archive objrefs
#
# Returns:
#   - hashref: date => backup line. Backup lines are represented as RB::Archive
#     arrayrefs. If backup line cannot be constructed (missing archive) then
#     it'll start with "undef" scalar.
sub constructBackupLines {
	my %bl = map { ($_->date, [ $_ ]) } @_;
	ITERATE_BACKUP_LINE_DATES:
	foreach my $date (keys %bl) {
		for (my $incDate = $bl{$date}->[0]->increment_from;
		    $incDate ne '';
		    $incDate = $bl{$date}->[0]->increment_from) {
			my $ar = $bl{$incDate}->[$#{$bl{$incDate}}];
			unshift(@{$bl{$date}}, $ar);
			next ITERATE_BACKUP_LINE_DATES unless defined $ar;
		}
	}
	return \%bl;
}


# Sets missing paramters ("compressed" and "format") in $params hashref.
#
# Arguments:
#   $infoPath  - absolute(!) path to archive information file 
#   $params    - hashref with detected info parameters
#                NB: $params->{'type'} should be set.
#   $saveFixes - boolean (true - routine should save fixes it made; false - not)
#   $fh        - filehandle where to save fixes made.
#
# Returns:
#   boolean - true if checked/updated sucessfully, false if not
sub detectArchiveFormat($$$;$) {
	my ($infoPath, $params, $saveFixes, $fh) = (shift, shift, shift, shift);
	die "non-absolute path: $infoPath"
	    unless substr($infoPath, 0, 1) eq '/';
	my $path = $infoPath;
	$path =~ s/\.info$/*/;
	my %stats = map { $_ => [ stat($_) ] }
	    grep { $_ ne $infoPath } glob $path;
	my @items = sort { $stats{$b}->[9] <=> $stats{$a}->[9] } keys %stats;
	while (@items) {
		my $item = shift @items;
		my ($comp, $format) = ($params->{'$comp'}, $params->{'format'});
		my $nfixed = 0;

		# Detect/check if archive is compressed
		if (substr($item, -3) eq '.gz') {
			if (defined $comp) {
				if ($comp eq 'no') {
					next;
				}
			} else {
				$comp = 'yes';
				$nfixed++;
			}
			$item = substr($item, 0, -3);    # Cut '.gz' suffix
		} else {
			if (defined $comp) {
				if ($comp eq 'yes') {
					next;
				}
			} else {
				$comp = 'no';
				$nfixed++;
			}
		}

		# Detect/check archive format
		if ($params->{'type'} eq 'files') {
			if (substr($item, -4) eq '.tar') {
				$format = 'tar';
			} elsif (substr($item, -5) eq '.cpio') {
				$format = 'cpio';
			} else {
				next;
			}
			$nfixed++ unless defined $params->{'format'};
		} elsif ($params->{'type'} eq 'mysql') {
			if (substr($item, -4) eq '.sql') {
				$format = 'plain';
			} else {
				next;
			}
			$nfixed++ unless defined $params->{'format'};
		} elsif ($params->{'type'} eq 'postgres') {
			if (substr($item, -4) eq '.sql') {
				$format = 'plain';
			} elsif (substr($item, -4) eq '.tar') {
				$format = 'tar';
			} elsif (substr($item, -3) eq '.pg') {
				$format = 'custom';
			} else {
				next;
			}
			$nfixed++ unless defined $params->{'format'};
		} elsif ($params->{'type'} eq 'fs') {
			if (substr($item, -4) eq '.dump') {
				$format = 'fs';
			} else {
				next;
			}
			$nfixed++ unless defined $params->{'format'};
		}
		if (defined $params->{'format'} and
		    $params->{'format'} ne $format) {
			next;
		}

		if (!defined $params->{'compressed'} and
		    defined $comp) {
			logIt(($saveFixes ? 2 : 1), "                ".
			    "compressed missing, setting to $comp");
			print $fh "compressed = ${comp}\n" if defined $fh;
		}
		if (!defined $params->{'format'} and
		    defined $format) {
			logIt(($saveFixes ? 2 : 1), "                ".
			    "format missing, setting to $format");
			print $fh "format = ${format}\n" if defined $fh;
		}

		if (defined($comp) and defined($format)) {
			$params->{'compressed'} = $comp;
			$params->{'format'} = $format;
			return $nfixed;
		}
	}
	return 0;
}

# Reads archive information file specified into hashref which will be returned.
#
# Arguments:
#   $infoPath  - absolute(!) path to archive information file 
#   $module    - backup module structure, used to set defaults
#   $saveFixes - boolean (true - routine should save fixes it made; false - not)
#
# Returns:
#   In scalar context:
#     - hashref with parameters from information file.
#   In list context:
#     - hashref with parameters from information file and "fixed" flag.
sub processArchiveInfo($$;$) {
	my ($infoPath, $module, $saveFixes) = (shift, shift, shift);

	my @stat = stat($infoPath);
	unless (@stat) {
		my $ec = $!+0;
		logIt(0, "    Error: cannot access ${infoPath} stats: $!");
		$! = $ec;
		return wantarray ? (undef, 0) : undef;
	}

	my $fh;
	my $ro = (!$saveFixes or ${main::Config}->{'test_mode'});
	unless (open($fh, ($ro ? '<' : '+<'), $infoPath)) {
		my $ec = $!+0;
		logIt(0, "    Error: cannot open ${infoPath}: $!");
		$! = $ec;
		return wantarray ? (undef, 0) : undef;
	}
	logIt(2, "    Archive info file $infoPath opened");

	my (%params, $hasNewLine);
	while (my $line = <$fh>) {
		chomp $line;
		if ($line =~ /^[\s]*(#.*)?$/) {
			# Skip blank and commented lines
			next;
		}
		unless ($line =~ /^[\s]*([a-zA-Z_0-9-]+)[\s]*=[\s]*([\S]+([\s]+[\S]+)*)[\s]*/) {
			logIt(0, "        Error in the info file ".
			    "$infoPath on line $., skipped");
			next;
		}
		my ($param, $value) = (lc $1, $2);
		$params{$param} = $value;
		# We do not test string for anything after code, to make
		# possible backward compatibility in future.
		if ($param eq 'exit_code') {
			logIt (2, "        Found exit code $value at line $.");
			# Do not stop scanning here because archive
			# could be overwritten.
		} elsif ($param eq 'begin') {
			logIt (2, "        Found backup start mark at line $.");
			# Backup was overwritten, reset
			%params = ($param, $value);
		}
	}
	logIt(2, "    Archive info file $infoPath read");
	print $fh "\n" unless $ro or $hasNewLine;

	my $fixedCount = 0;
	unless (exists $params{'begin'}) {
		my $date;
		if (exists $params{'end'}) {
			$date = $params{'end'};
		} else {
			$date = strftime('%Y-%m-%d %H:%M:%S',
			    localtime($stat[9]));
		}
		logIt($saveFixes ? 2 : 1, "                ".
		    "begin missing, setting begin to $date");
		if ($saveFixes and !${main::Config}->{'test_mode'}) {
			# Duplicate other params too!
			print $fh join("\n", "begin = ${date}",
			    map { "$_ = ".$params{$_} } keys %params),
			    "\n";
		}
		$params{'begin'} = $date;
		$fixedCount++;
	}
	unless (exists $params{'type'}) {
		my $type = $module->{'what'};
		logIt($saveFixes ? 2 : 1, "                ".
		    "type missing, setting to $type");
		print $fh "type = ${type}\n"
		    if $saveFixes and !${main::Config}->{'test_mode'};
		$params{'type'} = $type;
		$fixedCount++;
	}
	if ($params{'compressed'} ne 'yes' and $params{'compressed'} ne 'no') {
		# Force detecting "right" compression value
		delete $params{'compressed'};
	}
	unless (exists $params{'format'} and exists $params{'compressed'}) {
		$fixedCount += detectArchiveFormat($infoPath, \%params,
		    $saveFixes, ($ro ? undef : $fh));
	}
	unless (exists $params{'format'}) {
		my $format = $module->{'archive_format'};
		logIt($saveFixes ? 2 : 1, "                ".
		    "format missing, setting to $format");
		print $fh "format = ${format}\n"
		    if $saveFixes and !${main::Config}->{'test_mode'};
		$params{'format'} = $format;
		$fixedCount++;
	}
	unless (exists $params{'compressed'}) {
		my $comp = $module->{'compress'} ? "yes" : "no";
		logIt($saveFixes ? 2 : 1, "                ".
		    "compressed missing, setting to ".
		    $comp);
		print $fh "compressed = ${comp}\n"
		    if $saveFixes and !${main::Config}->{'test_mode'};
		$params{'compressed'} = $comp;
		$fixedCount++;
	}
	unless (exists $params{'extension'}) {
		my $ext = constructArchiveExt(
		    $params{'type'},
		    $params{'format'},
		    $params{'compressed'});
		logIt($saveFixes ? 2 : 1, "                ".
		    "extension missing, setting to $ext");
		print $fh "extension = ${ext}\n"
		    if $saveFixes and !${main::Config}->{'test_mode'};
		$params{'extension'} = $ext;
		$fixedCount++;
	}
	unless (exists $params{'exit_code'}) {
		# Optimistic, yeah
		logIt($saveFixes ? 2 : 1, "                ".
		    "exit_code missing, setting to 0");
		print $fh "exit_code = 0\n"
		    if $saveFixes and !${main::Config}->{'test_mode'};
		$params{'exit_code'} = 0;
		$fixedCount++;
	}
	unless (exists $params{'end'}) {
		my $date = strftime('%Y-%m-%d %H:%M:%S',
		    localtime($stat[9]));
		logIt($saveFixes ? 2 : 1, "                ".
		    "end missing, setting to $date");
		print $fh "end = ${date}\n"
		    if $saveFixes and !${main::Config}->{'test_mode'};
		$params{'end'} = $date;
		$fixedCount++;
	}

	close $fh;
	logIt(2, "    Archive info file $infoPath closed, ".
	    "$fixedCount parameters fixed");

	if (wantarray) {
		return (\%params, $fixedCount);
	} else {
		return \%params;
	}
}


# Scans given directory for subdirs (e.g. users, DBs...) and applies all layers
#
# Arguments:
#   $dir    - directory to scan (with its subdirectories)
#          NB: should be an absolute path or the same as working directory.
#   $module - backup module structure which corresponds to $dir
#
# Returns:
#   - three-level hashref with all items found: item->date->RB::Archive
#   - three-level hashref with OK dumped items found: item->date->RB::Archive
sub scanBackupDir($$) {
	return (_scanBackupDirMain($_[0], $_[1], ''));
}


# Internal ("real") routine used for scanBackupDir() implementing. Recursive.
#
# Arguments:
#   $dir              - same as above
#   $module           - same as above
#   $goneThoroughDirs - concatenated list of $dir subdirectories we scanned
#
# Returns:
#   - three-level hashref with all items found: item->date->RB::Archive
#   - three-level hashref with OK dumped items found: item->date->RB::Archive
sub _scanBackupDirMain($$$) {
	my ($baseDir, $module, $goneThroughDirs) = (shift, shift, shift);
	my ($all, $ok) = ({}, {});

	my $thisDir = $baseDir.$goneThroughDirs;
	my $dh;
	unless (opendir($dh, $thisDir)) {
		logIt(0, "    Cannot read base directory ".
		    $thisDir.": $!");
		next;
	}
	my ($complexDetected, $plainDetected) = (0, 0);
	logIt (1, "    Scanning directory $thisDir");
	while (my $f = readdir $dh) {
		next unless $f ne '.' && $f ne '..';
		my $path = $thisDir.'/'.$f;

		if (-d $path) {
			unless ($complexDetected) {
				logIt(2, "    Complex directory detected: ".
				    $thisDir);
				$complexDetected = 1;
			}

			# First test if it is complex directory
			# ("separate_archives" option is on), next if it is
			# plain (off).
			my ($allSub, $okSub) = _scanBackupDirMain($baseDir,
			    $module, $goneThroughDirs.'/'.$f);
			if ($allSub && $okSub && %{$allSub}) {
				# Complex directory
				#
				# Each $item will contain $goneThroughDirs
				# already.
				foreach my $item (keys %{$allSub}) {
					$all->{$item} = $allSub->{$item};
				}
				foreach my $item (keys %{$okSub}) {
					$ok->{$item} = $okSub->{$item};
				}
			}
		} elsif (-f $path and -r $path and
		    $f =~ /^([0-9]{4}[0-9]{2}[0-9]{2})\.info$/) {
			my $date = $1;
			next unless $date <= $Today;    # You can do some
			                                # tricks, if you want,
			                                # using this check.

			# It's plain directory too
			unless ($plainDetected) {
				logIt(1, "    Plain directory detected: ".
				    $thisDir);
				$plainDetected = 1;
			}

			# Cut leading "/"
			my $itemDir = substr($goneThroughDirs, 1);

			logIt (1, "        Reading info for $date");
			my $params = processArchiveInfo($path, $module);
			next unless $params;

			# Add special info that shouldn't be accidentally
			# overwritten
			$params->{'date'} = $date;
			$params->{'basePath'} = substr($path, 0, -5);

			my $infoObj = new RB::Archive($params);
			$all->{$itemDir}->{$date} = $infoObj;
			$ok->{$itemDir}->{$date} = $infoObj
			    if $infoObj->dumpedOK;
		}
	}
	close $dh;
	logIt (1, "    Directory $thisDir scanning ended");
	return ($all, $ok);
}


# Prints list of dates of existsing items backups. Marks unsuccessfully dumped
# items with "*" after date.
#
# Arguments:
#   $all         - list of all backup items.
#   $ok          - list of only backed up successfully items.
#   $whichFilter - filter to be used on item names
#
# Returns: none
sub printArchiveContents($$$) {
	my ($all, $ok, $whichFilter) = (shift, shift, shift);
	my $source = (${main::Config}->{'ok_only'} ? $ok : $all);
	foreach my $item (sort keys %{$source}) {
		next if $item ne '' and $item !~ /^$whichFilter$/;
		unless (${main::Config}->{'quiet'}) {
			if ($item eq '') {
				print "    Backup list: ";
			} else {
				print "    Backup list for item ${item}: ";
			}
		} elsif ($item ne '') {
			print "${item}: ";

		}
		my $okItem = $ok->{$item};
		my $first = 1;
		foreach my $date (sort keys %{$source->{$item}}) {
			print ', ' unless $first;
			print $date;
			print '*' unless exists $okItem->{$date};
			$first = 0;
		}
		print "\n";
	}
}


1;
